Scala - NonLocalReturnControl

状态说明

今天跑 Spark 作业的时候,刚进入 RUNNING 状态没多久就直接抛出了下面这种异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
User class threw exception: org.apache.spark.SparkException: Task not serializable
at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:298)
at org.apache.spark.util.ClosureCleaner$.org$apache$spark$util$ClosureCleaner$$clean(ClosureCleaner.scala:288)
at org.apache.spark.util.ClosureCleaner$.clean(ClosureCleaner.scala:108)
at org.apache.spark.SparkContext.clean(SparkContext.scala:2100)
.....
Caused by: java.io.NotSerializableException: java.lang.Object
Serialization stack:
- object not serializable (class: java.lang.Object, value: java.lang.Object@65c9e3ee)
- field (class: com.xiaomi.search.websearch.hbase.SegTitlePick$$anonfun$1, name: nonLocalReturnKey1$1, type: class java.lang.Object)
- object (class com.xiaomi.search.websearch.hbase.SegTitlePick$$anonfun$1, <function1>)
at org.apache.spark.serializer.SerializationDebugger$.improveException(SerializationDebugger.scala:40)
at org.apache.spark.serializer.JavaSerializationStream.writeObject(JavaSerializer.scala:46)
at org.apache.spark.serializer.JavaSerializerInstance.serialize(JavaSerializer.scala:100)
at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:295)

上网一查发现时某个匿名函数里面使用了 return 导致的。

报错理由是什么呢

源代码就不贴出来了,我们以一个简单的例子来说明这个问题吧。

1
2
3
4
5
6
7
8
9
10
11
object Test {
def main(args: Array[String]): Unit = {
val datas = List(1, 2, 3, 4)
datas.foreach(t => {
if (t % 2 == 0) return // 运行符合条件时便立刻返回
})

// 本例的目标想在遍历完 datas 后便输出该语句,但在实际情况下,return 语句会直接返回并退出当前函数(即 main 函数),所以以下语句并不会输出结果
println("finished!")
}
}

让我们查看编译后这段遍历的代码有什么不一样的地方吧?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// scalac -Xprint:explicitouter Test.scala
def main(args: Array[String]): Unit = {
<synthetic> val nonLocalReturnKey1: Object = new Object();
try {
val datas: List[Int] = scala.collection.immutable.List.apply[Int] (scala.Predef.wrapIntArray(Array[Int]{1, 2, 3, 4}));
datas.foreach[Unit]({
final <artifact> def $anonfun$main(t: Int): Unit = if (t.%(2).==(0))
throw new scala.runtime.NonLocalReturnControl$mcV$sp(nonLocalReturnKey1, ())
else
();
((t: Int) => $anonfun$main(t))
});
scala.Predef.println("finished!")
} catch {
case (ex @ (_: scala.runtime.NonLocalReturnControl[Unit @unchecked])) => if (ex.key().eq(nonLocalReturnKey1))
ex.value$mcV$sp()
else
throw ex
}
}

编译后我们可以看到原先匿名函数中的 return 语句被替换成抛出一个NonLocalReturnControl运行时异常,而try-catch环绕着整个 main 函数内部的代码块来尝试捕获这个异常。

而观察NonLocalReturnControl异常,我们发现这个异常是无法被序列化的,这就解释了之前的作业抛出异常的意思了。

为什么 return 语句要这么设计呢

为什么 Scala 要这么做呢?这里有几篇不错的文章来说明,我就偷懒不去翻译了(建议从上往下看)

  1. 介绍什么是 non-local return - https://www.zhihu.com/question/22240354/answer/64673094
  2. 前半段介绍 return 语句该什么时候出现,后半段推测出这么做的两个原因 - https://stackoverflow.com/questions/17754976/scala-return-statements-in-anonymous-functions
  3. 讨论在 Scala 中 function 和 method 两者概念上的区别 - https://link.jianshu.com/?t=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F2529184%2Fdifference-between-method-and-function-in-scala

但其实翻阅了网上的资料,并没有真正地说明为什么这么设计。结合上面的几篇文章,我个人认为在 Scala 这一门函数式编程语言里,其更加讲究的是程序执行的结果,而并非执行过程。return 语句影响程序的顺序执行,从而可能会使代码变得复杂,也可能会发生若干次程序执行的结果不一致的情况,那么这将在很大程度上影响了我们对于代码的理解与认识。这也是 Scala 为什么不倡导我们使用 return。